package com.javaxiaobear.base.common.utils;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.format.FastDateFormat;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.javaxiaobear.base.common.exception.ServiceException;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Date;
import java.util.TimeZone;
import lombok.EqualsAndHashCode;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@EqualsAndHashCode(callSuper = false)
public class SmartDateFormat extends DateFormat {

  static final String MIN_LONG_STR_NO_SIGN = String.valueOf(Long.MIN_VALUE).substring(1);
  static final String MAX_LONG_STR = String.valueOf(Long.MAX_VALUE);

  private final String dateTimeFormat;

  private final String[] supportFormats;

  public static final String[] DEFAULT_FORMATS =
      new String[] {
        Dates.DATETIME_MILL_FORMAT,
        Dates.DATE_SIMPLE_FORMAT,
        Dates.DATE_UNDERLINE_FORMAT,
        Dates.CTS_DATE_FORMAT,
        Dates.TIME_SIMPLE_FORMAT,
        Dates.TIME_FORMAT,
        Dates.TIME_PLUS_FORMAT,
        Dates.DATETIME_SHORT_MILL_FORMAT
      };

  public static final SmartDateFormat INSTANCE = new SmartDateFormat();

  @Override
  public SmartDateFormat clone() {
    return this;
  }

  public SmartDateFormat() {
    this(Dates.DEFAULT_FORMAT, DEFAULT_FORMATS);
  }

  public String getDateTimeFormat() {
    return dateTimeFormat;
  }

  public SmartDateFormat(String dateTimeFormat, String[] supportFormats) {
    this.dateTimeFormat = dateTimeFormat;
    this.supportFormats = supportFormats;
  }

  public SmartDateFormat(String dateTimeFormat) {
    this.dateTimeFormat = dateTimeFormat;
    this.supportFormats = DEFAULT_FORMATS;
  }

  @Override
  public void setTimeZone(TimeZone zone) {}

  @Override
  public TimeZone getTimeZone() {
    return Dates.DEFAULT_TIMEZONE;
  }

  private static void pad2(StringBuffer buffer, int value) {
    int tens = value / 10;
    if (tens == 0) {
      buffer.append('0');
    } else {
      buffer.append((char) ('0' + tens));
      value -= 10 * tens;
    }
    buffer.append((char) ('0' + value));
  }

  private static void pad3(StringBuffer buffer, int value) {
    int h = value / 100;
    if (h == 0) {
      buffer.append('0');
    } else {
      buffer.append((char) ('0' + h));
      value -= h * 100;
    }
    pad2(buffer, value);
  }

  private static void pad4(StringBuffer buffer, int value) {
    int h = value / 100;
    if (h == 0) {
      buffer.append('0').append('0');
    } else {
      if (h > 99) {
        buffer.append(h);
      } else {
        pad2(buffer, h);
      }
      value -= 100 * h;
    }
    pad2(buffer, value);
  }

  @Override
  @SneakyThrows
  public DateTime parse(String dateStr) {
    return parse(dateStr, new ParsePosition(0));
  }

  @Override
  public DateTime parse(String dateStr, ParsePosition pos) {
    if (StrUtil.isEmpty(dateStr)) {
      return null;
    }

    if (Dates.DATETIME_SIMPLE_FORMAT.length() == dateStr.length() && StrUtil.isNumeric(dateStr)) {
      return DateUtil.parse(dateStr, Dates.DATETIME_SIMPLE_FORMAT);
    } else if (looksLikeDateTimeFormat(dateStr, Dates.DATETIME_FORMAT)) {
      return DateUtil.parse(dateStr, Dates.DATETIME_FORMAT);
    } else if (looksLikeDateTimePlusFormat(dateStr, Dates.DATETIME_PLUS_FORMAT)) {
      return DateUtil.parse(dateStr, Dates.DATETIME_PLUS_FORMAT);
    } else if (looksLikeDateFormat(dateStr, Dates.DATE_FORMAT)) {
      return DateUtil.parse(dateStr, Dates.DATE_FORMAT);
    } else if (looksLikeISO8601(dateStr)) {
      if (!dateStr.contains(".") && dateStr.length() < 20) {
        return DateUtil.parse(dateStr, Dates.DATE_FORMAT_ISO8601_SHORT);
      } else if (dateStr.lastIndexOf("+") < 0) {
        return DateUtil.parse(dateStr, Dates.DATE_FORMAT_ISO8601_LITE);
      } else {
        return DateUtil.parse(dateStr, Dates.DATE_FORMAT_ISO8601);
      }
    }
    int i = dateStr.length();
    while (--i >= 0) {
      char ch = dateStr.charAt(i);
      if (ch < '0' || ch > '9') {
        // 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work
        if (i > 0 || ch != '-') {
          break;
        }
      }
    }
    if ((i < 0)
        // let's just assume negative numbers are fine (can't be RFC-1123 anyway); check length for
        // positive
        && (dateStr.charAt(0) == '-' || NumberUtil.isNumber(dateStr))) {
      return _parseDateFromLong(dateStr, pos);
    }

    return Dates.parse(dateStr, supportFormats);
  }

  private DateTime _parseDateFromLong(String longStr, ParsePosition pos) {
    long ts;
    try {
      ts = NumberUtil.parseLong(longStr);
    } catch (NumberFormatException e) {
      throw new ServiceException(
          String.format("Timestamp value %s out of 64-bit value range", longStr));
    }
    return new DateTime(ts);
  }

  @Override
  public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
    return FastDateFormat.getInstance(dateTimeFormat).format(date, toAppendTo, fieldPosition);
  }

  private boolean looksLikeDateFormat(String dateStr, String format) {
    return looksLikeDateFormat(dateStr, format.length());
  }

  private boolean looksLikeDateTimePlusFormat(String dateStr, String format) {
    return looksLikeDateFormat(dateStr, format.length()) && dateStr.charAt(10) == '+';
  }

  private boolean looksLikeDateTimeFormat(String dateStr, String format) {
    return looksLikeDateFormat(dateStr, format) && dateStr.charAt(10) == ' ';
  }

  private boolean looksLikeDateFormat(String dateStr, int len) {
    return len == dateStr.length()
        && Character.isDigit(dateStr.charAt(0))
        && Character.isDigit(dateStr.charAt(3))
        && dateStr.charAt(4) == '-';
  }

  protected boolean looksLikeISO8601(String dateStr) {
    return dateStr.length() >= 7
        && dateStr.indexOf("T") > 0
        && Character.isDigit(dateStr.charAt(0))
        && Character.isDigit(dateStr.charAt(3))
        && dateStr.charAt(4) == '-'
        && Character.isDigit(dateStr.charAt(5));
  }

  public static boolean inLongRange(String s, boolean negative) {
    String cmp = negative ? MIN_LONG_STR_NO_SIGN : MAX_LONG_STR;
    int cmpLen = cmp.length();
    int alen = s.length();
    if (alen < cmpLen) {
      return true;
    } else if (alen > cmpLen) {
      return false;
    } else {
      for (int i = 0; i < cmpLen; ++i) {
        int diff = s.charAt(i) - cmp.charAt(i);
        if (diff != 0) {
          return diff < 0;
        }
      }

      return true;
    }
  }
}
